import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useCallback, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useConversation } from '../../src/hooks'; import { colors } from '../../src/theme'; import type { ChatCard, ChatMessage } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ contactName, companyName, positionName, avatar, online, onBack, onMore, }: { contactName: string; companyName: string; positionName: string; avatar: string; online: boolean; onBack: () => void; onMore: () => void; }) { return ( {avatar} {contactName} {online && } {companyName} · {positionName} ); } /* ===================== 子组件:双向匹配横幅 ===================== */ function MatchBanner({ score, contactName }: { score: number; contactName: string }) { return ( 恭喜!您与{contactName}双向匹配成功 匹配度 {score}% · 立即沟通 ); } /* ===================== 子组件:消息气泡 ===================== */ function Avatar({ text }: { text: string }) { return ( {text} ); } function TextBubble({ message, isUser, }: { message: ChatMessage; isUser: boolean; }) { return ( {message.content} ); } function SystemMessage({ content }: { content: string }) { return ( {content} ); } /* ===================== 子组件:富消息卡片 ===================== */ function InterviewCard({ card }: { card: ChatCard }) { return ( {card.title} {card.items?.map((item, idx) => ( {idx + 1} {item} ))} console.log('[Chat] 查看全部预测题')}> {card.actionText} ); } function SalaryCard({ card }: { card: ChatCard }) { return ( {card.title} {card.subtitle && ( {card.subtitle} )} console.log('[Chat] 查看薪资报告')}> {card.actionText} ); } function OfferCard({ card }: { card: ChatCard }) { const router = useRouter(); return ( {card.title} {card.subtitle && {card.subtitle}} router.push('/jobs/offer-compare')}> {card.actionText} ); } function MessageCard({ card }: { card: ChatCard }) { switch (card.type) { case 'interview': return ; case 'salary': return ; case 'offer': return ; default: return null; } } /* ===================== 子组件:单条消息行 ===================== */ function MessageRow({ message, avatar, }: { message: ChatMessage; avatar: string; }) { const isUser = message.role === 'user'; const isSystem = message.role === 'system'; if (isSystem) { return ; } return ( {!isUser && } {message.content ? ( ) : null} {message.card ? : null} ); } /* ===================== 子组件:时间戳 ===================== */ function Timestamp({ time }: { time: string }) { return ( {time} ); } /* ===================== 子组件:底部输入框 ===================== */ function InputBar({ value, onChange, onSend, sending, }: { value: string; onChange: (text: string) => void; onSend: () => void; sending: boolean; }) { return ( console.log('[Chat] 表情')}> console.log('[Chat] 图片')}> console.log('[Chat] 文件')}> console.log('[Chat] 更多')}> ); } /* ===================== 页面 ===================== */ export default function ChatDetailScreen() { const router = useRouter(); const { id } = useLocalSearchParams<{ id: string }>(); const { detail, loading, error, refresh, sendMessage, sending } = useConversation(id ?? ''); const [input, setInput] = useState(''); const scrollRef = useRef(null); const messages = useMemo(() => detail?.messages ?? [], [detail]); const handleSend = useCallback(async () => { const text = input.trim(); if (!text || sending) return; setInput(''); await sendMessage(text); // 滚动到底部 setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100); }, [input, sending, sendMessage]); if (loading) { return ( ); } if (error || !detail) { return ( {error ? '加载失败' : '会话不存在'} 重试 ); } return (
router.back()} onMore={() => console.log('[ChatDetail] 更多操作')} /> {detail.matchInfo && ( )} scrollRef.current?.scrollToEnd({ animated: false })} > {messages.map((message, index) => { const showTime = index === 0 || messages[index - 1].timestamp !== message.timestamp || message.role !== messages[index - 1].role; return ( {showTime && message.timestamp ? ( ) : null} ); })} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, flex: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, height: 52, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerCenter: { flexDirection: 'row', alignItems: 'center', flex: 1, justifyContent: 'center', }, headerInfo: { marginLeft: 10, alignItems: 'center', }, headerNameRow: { flexDirection: 'row', alignItems: 'center', }, headerName: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, }, onlineDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.success, marginLeft: 6, }, headerSub: { marginTop: 2, fontSize: 12, color: colors.textSecondary, }, avatar: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, avatarText: { fontSize: 14, fontWeight: '700', color: colors.white, }, banner: { flexDirection: 'row', alignItems: 'center', marginHorizontal: 8, marginTop: 8, padding: 10, borderRadius: 8, backgroundColor: colors.successLight, }, bannerIconWrap: { width: 34, height: 34, borderRadius: 17, backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', marginRight: 10, }, bannerBody: { flex: 1, }, bannerTitle: { fontSize: 14, fontWeight: '700', color: colors.successText, }, bannerMeta: { marginTop: 2, fontSize: 12, color: colors.success, }, scroll: { flex: 1, }, scrollContent: { padding: 8, paddingBottom: 8, }, systemWrap: { alignItems: 'center', marginVertical: 10, }, systemText: { fontSize: 12, color: colors.textHint, }, timeWrap: { alignItems: 'center', marginVertical: 6, }, timeText: { fontSize: 11, color: colors.textHint, }, messageRow: { flexDirection: 'row', marginBottom: 10, maxWidth: '82%', }, messageRowLeft: { alignSelf: 'flex-start', }, messageRowRight: { alignSelf: 'flex-end', flexDirection: 'row-reverse', }, messageBody: { marginLeft: 8, marginRight: 8, }, bubble: { paddingHorizontal: 12, paddingVertical: 9, borderRadius: 12, maxWidth: '100%', }, bubbleHr: { backgroundColor: colors.white, borderBottomLeftRadius: 4, }, bubbleUser: { backgroundColor: colors.primary, borderBottomRightRadius: 4, }, bubbleText: { fontSize: 14, lineHeight: 20, }, bubbleTextHr: { color: colors.textPrimary, }, bubbleTextUser: { color: colors.white, }, cardBase: { width: 260, backgroundColor: colors.white, borderRadius: 10, padding: 12, marginTop: 6, }, cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, cardTitle: { marginLeft: 6, fontSize: 14, fontWeight: '700', color: colors.textPrimary, }, cardSalary: { backgroundColor: colors.primaryLight, }, cardList: { marginBottom: 8, }, cardListRow: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 6, }, cardBullet: { width: 16, height: 16, borderRadius: 8, backgroundColor: colors.successLight, alignItems: 'center', justifyContent: 'center', marginRight: 6, marginTop: 2, }, cardBulletText: { fontSize: 10, fontWeight: '700', color: colors.success, }, cardItemText: { flex: 1, fontSize: 13, color: colors.textPrimary, }, cardSubtitle: { fontSize: 12, lineHeight: 18, color: colors.textSecondary, marginBottom: 10, }, cardAction: { flexDirection: 'row', alignItems: 'center', paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.borderLight, }, cardActionText: { flex: 1, fontSize: 12, color: colors.primary, fontWeight: '600', }, cardOffer: { width: 260, backgroundColor: colors.actionLight, borderRadius: 10, padding: 12, marginTop: 6, }, cardOfferHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, }, cardOfferTitleWrap: { flex: 1, marginLeft: 10, }, cardOfferTitle: { fontSize: 14, fontWeight: '700', color: colors.actionText, }, cardOfferSubtitle: { marginTop: 2, fontSize: 12, color: colors.textSecondary, }, cardOfferBtn: { flexDirection: 'row', alignItems: 'center', paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.actionBorder, }, cardOfferBtnText: { flex: 1, fontSize: 12, color: colors.actionText, fontWeight: '600', }, inputBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 6, paddingVertical: 8, backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.borderLight, }, inputLeftActions: { flexDirection: 'row', alignItems: 'center', }, inputIconBtn: { width: 34, height: 34, alignItems: 'center', justifyContent: 'center', }, inputWrap: { flex: 1, flexDirection: 'row', alignItems: 'center', marginHorizontal: 4, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 18, backgroundColor: colors.pageBg, }, input: { flex: 1, maxHeight: 80, minHeight: 34, fontSize: 14, color: colors.textPrimary, paddingRight: 8, }, plusBtn: { width: 28, height: 28, alignItems: 'center', justifyContent: 'center', }, sendBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primaryBtn, }, sendBtnDisabled: { opacity: 0.5, }, stateBox: { flex: 1, alignItems: 'center', justifyContent: 'center', }, stateText: { marginTop: 12, fontSize: 14, color: colors.textSecondary, }, retryBtn: { marginTop: 16, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.white, }, });